package org.teamh.gui.engine.model;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;
import java.util.StringTokenizer;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GLContext;

import static org.teamh.gui.engine.model.VBO.*;

/**
 * This loader is explicit responsible for loading the obj files. 
 * @author richard
 *
 */
public class OBJLoader {
	private static HashMap<String, Model> cache = new HashMap<String, Model>();
	private static int texture = 0;
	private static Material material = null;
	
	private OBJLoader() {
		
	}
	
	/**
	 * Return any file, which is in connection with the file. 
	 * @param file
	 * @return
	 */
	public static File[] getOBJFiles(File file) {
		File matFile = null;
		ArrayList<File> imageFiles = new ArrayList<File>();
		
		Scanner scanner = null;
		try {
			scanner = new Scanner(file);
		} catch (FileNotFoundException e) {
			return null;
		}
		String line = null;

		while (scanner.hasNext()) {
			line = scanner.nextLine();

			if (line.startsWith("mtllib ")) {
				int firstSpace = line.indexOf(" ");
				String name = line.substring(firstSpace + 1);

				for (File f : file.getParentFile().listFiles()) {
					if (f.getName().equals(name)) {
						matFile = f;
					}
				}
				
				break;
			}
		}
		
		scanner.close();
		
		try {
			scanner = new Scanner(matFile);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
			return null;
		}
		
		while(scanner.hasNext()) {
			line = scanner.nextLine();
			
			if(line.startsWith("map_Kd ")) {
				int firstSpace = line.indexOf(" ");
				String name = line.substring(firstSpace + 1);

				for (File f : file.getParentFile().listFiles()) {
					if (f.getName().equals(name)) {
						imageFiles.add(f);
					}
				}
			}
		}
		
		scanner.close();
		
		File[] returnValue = new File[2 + imageFiles.size()];
		returnValue[0] = file;
		returnValue[1] = matFile;
		
		for(int i = 2; i < returnValue.length; i++) {
			returnValue[i] = imageFiles.get(i - 2);
		}
		
		return returnValue;
	}

	/**
	 * Returns a model from an obj file. 
	 * @param file
	 * @return
	 */
	public static Model getOBJ(File file) {
		if (cache.containsKey(file.getPath())) {
			Model oldModel = cache.get(file.getPath());
			OBJMesh oldMesh = (OBJMesh)oldModel.getMeshes()[0];
			OBJMesh newMesh = new OBJMesh(oldMesh.getBuffers(), oldMesh.getNumVertices());
			newMesh.setMaterial(oldMesh.getMaterial());
			newMesh.setTexture(oldMesh.getTexture());
			return new Model(new OBJMesh[]{newMesh});
		}

		texture = 0;
		material = null;

		InputStream inputStream = null;
		try {
			inputStream = new FileInputStream(file);
		} catch (FileNotFoundException e) {
			System.out.println("Cannot find " + file.getAbsolutePath());
			return null;
		}

		// Step 1: Analyze size of the arrays
		Scanner scanner = new Scanner(inputStream);
		String line = null;

		int numVertices = 0;
		int numNormals = 0;
		int numTexCoords = 0;
		int numFaces = 0;

		while (scanner.hasNext()) {
			line = scanner.nextLine();

			if (line.startsWith("v ")) {
				numVertices++;
			} else if (line.startsWith("vn ")) {
				numNormals++;
			} else if (line.startsWith("vt ")) {
				numTexCoords++;
			} else if (line.startsWith("f ")) {
				numFaces++;
			}
		}

		scanner.close();

		// Step 2: read and recycle raw data
		float[] tmpVertices = new float[numVertices * 3];
		int verticesCounter = 0;
		float[] tmpNormals = new float[numNormals * 3];
		int normalsCounter = 0;
		float[] tmpTexCoords = new float[numTexCoords * 2];
		int texCoordCounter = 0;
		int[] indices = new int[numFaces * 9];
		int faceCounter = 0;

		try {
			inputStream = new FileInputStream(file);
		} catch (FileNotFoundException e) {
			System.out.println("Cannot find " + file.getAbsolutePath());
			return null;
		}
		scanner = new Scanner(inputStream);

		while (scanner.hasNext()) {
			line = scanner.nextLine();

			if (line.startsWith("v ")) {
				verticesCounter = scanValues(line, tmpVertices, verticesCounter);
			} else if (line.startsWith("vn ")) {
				normalsCounter = scanValues(line, tmpNormals, normalsCounter);
				normalizeLastVector(tmpNormals, normalsCounter);
			} else if (line.startsWith("vt ")) {
				texCoordCounter = scanValues(line, tmpTexCoords,
						texCoordCounter);
			} else if (line.startsWith("f ")) {
				faceCounter = scanFace(line, indices, faceCounter);
			} else if (line.startsWith("mtllib ")) {
				loadMaterial(file, line);
			}
		}

		scanner.close();

		// Step 3: insert raw data into array
		float[] v = new float[numFaces * 9];
		float[] t = new float[numFaces * 6];
		float[] n = new float[numFaces * 9];

		for (int i = 0; i < numFaces; i++) {
			for (int j = 0; j < 3; j++) {
				v[i * 9 + j * 3] = tmpVertices[indices[i * 9 + j * 3] * 3];
				v[i * 9 + j * 3 + 1] = tmpVertices[indices[i * 9 + j * 3] * 3 + 1];
				v[i * 9 + j * 3 + 2] = tmpVertices[indices[i * 9 + j * 3] * 3 + 2];

				t[i * 6 + j * 2] = tmpTexCoords[indices[i * 9 + j * 3 + 1] * 2];
				t[i * 6 + j * 2 + 1] = tmpTexCoords[indices[i * 9 + j * 3 + 1] * 2 + 1];

				n[i * 9 + j * 3] = tmpNormals[indices[i * 9 + j * 3 + 2] * 3];
				n[i * 9 + j * 3 + 1] = tmpNormals[indices[i * 9 + j * 3 + 2] * 3 + 1];
				n[i * 9 + j * 3 + 2] = tmpNormals[indices[i * 9 + j * 3 + 2] * 3 + 2];
			}
		}

		// Step 4: build VBO's

		if (GLContext.getCapabilities().GL_ARB_vertex_buffer_object) {
			int[] buffers = buildVBOs(v, t, n);
			OBJMesh mesh = new OBJMesh(buffers, numFaces * 3);
			mesh.setTexture(texture);
			mesh.setMaterial(material);
			
			Model model = new Model(new OBJMesh[]{mesh});

			cache.put(file.getPath(), model);
			return model;
		} else {
			System.out.println("System does not support vertex buffer objects");
		}

		return null;
	}

	/**
	 * Scans a value from the text file. 
	 * @param line
	 * @param array
	 * @param position
	 * @return
	 */
	private static int scanValues(String line, float[] array, int position) {
		int firstSpace = line.indexOf(" ");
		StringTokenizer tok = new StringTokenizer(line
				.substring(firstSpace + 1), " ");
		while (tok.hasMoreElements()) {
			array[position++] = Float.parseFloat(tok.nextToken());
		}

		return position;
	}

	/**
	 * Normalises the vector on the specified position. 
	 * @param array
	 * @param position
	 */
	private static void normalizeLastVector(float[] array, int position) {
		float x = array[position - 3];
		float y = array[position - 2];
		float z = array[position - 1];

		double length = Math.sqrt(x * x + y * y + z * z);
		array[position - 3] /= length;
		array[position - 2] /= length;
		array[position - 1] /= length;
	}

	/**
	 * Scans a face from the obj file. 
	 * @param line
	 * @param array
	 * @param position
	 * @return
	 */
	private static int scanFace(String line, int[] array, int position) {
		int firstSpace = line.indexOf(" ");
		StringTokenizer tok = new StringTokenizer(line
				.substring(firstSpace + 1), " /");

		while (tok.hasMoreElements()) {
			array[position++] = Integer.parseInt(tok.nextToken()) - 1;
		}

		return position;
	}

	/**
	 * Loads the material file of an obj file. 
	 * @param file
	 * @param line
	 */
	private static void loadMaterial(File file, String line) {
		int firstSpace = line.indexOf(" ");
		String name = line.substring(firstSpace + 1);

		File matFile = null;

		for (File f : file.getParentFile().listFiles()) {
			if (f.getName().equals(name)) {
				matFile = f;
			}
		}

		if (matFile == null) {
			return;
		}

		InputStream inputStream = null;
		try {
			inputStream = new FileInputStream(matFile);
		} catch (FileNotFoundException e) {
			return;
		}

		float[] ambient = null;
		float[] diffuse = null;
		float[] specular = null;
		float[] emmision = null;
		float shininess = 0;

		Scanner scanner = new Scanner(inputStream);
		while (scanner.hasNext()) {
			line = scanner.nextLine();

			if (line.startsWith("map_Kd ")) {
				scanTexture(file.getParentFile(), line);
			} else if (line.startsWith("Kd ")) {
				diffuse = scanLine(line, 4);
			} else if (line.startsWith("Ka ")) {
				ambient = scanLine(line, 4);
			} else if (line.startsWith("Ks ")) {
				specular = scanLine(line, 4);
			} else if (line.startsWith("Ke ")) {
				emmision = scanLine(line, 4);
			} else if (line.startsWith("Ns ")) {
				shininess = scanLine(line, 1)[0];
			}
		}
		scanner.close();

		material = new Material(ambient, diffuse, specular, emmision, shininess);
	}

	/**
	 * Reads a line and returns the number of arguments in a float array. 
	 * @param line
	 * @param number
	 * @return
	 */
	private static float[] scanLine(String line, int number) {
		int firstSpace = line.indexOf(" ");
		StringTokenizer tok = new StringTokenizer(line
				.substring(firstSpace + 1), " ");

		float[] ret = new float[number];
		for (int i = 0; i < ret.length; i++) {
			if (tok.hasMoreTokens()) {
				ret[i] = Float.parseFloat(tok.nextToken());
			} else {
				ret[i] = 1;
			}
		}

		return ret;
	}

	/**
	 * Reads the texture file. 
	 * @param parent
	 * @param line
	 */
	private static void scanTexture(File parent, String line) {
		int firstSpace = line.indexOf(" ");
		String name = line.substring(firstSpace + 1);

		texture = TextureLoader.loadTexture(parent.toString() + "/" + name,
				GL11.GL_LINEAR_MIPMAP_LINEAR, GL11.GL_LINEAR, GL11.GL_CLAMP, GL11.GL_CLAMP,
				true);
	}

	/**
	 * Creates the vbos for storage on the graphics card. 
	 * @param v
	 * @param t
	 * @param n
	 * @return
	 */
	private static int[] buildVBOs(float[] v, float[] t, float[] n) {
		FloatBuffer vertices = BufferUtils.createFloatBuffer(v.length).put(v);
		FloatBuffer normals = BufferUtils.createFloatBuffer(n.length).put(n);
		FloatBuffer texCoords = BufferUtils.createFloatBuffer(t.length).put(t);
		vertices.rewind();
		normals.rewind();
		texCoords.rewind();

		int[] buffers = new int[3];
		glGenBuffers(buffers);

		// Daten in den Graphikkartenspeicher kopieren
		glBindBuffer(buffers[0]);
		glBufferData(buffers[0], vertices);
		glBindBuffer(0);

		glBindBuffer(buffers[1]);
		glBufferData(buffers[1], normals);
		glBindBuffer(0);

		glBindBuffer(buffers[2]);
		glBufferData(buffers[2], texCoords);
		glBindBuffer(0);

		vertices.clear();
		normals.clear();
		texCoords.clear();

		return buffers;
	}

	/**
	 * Removes any model from the graphics card. 
	 */
	public static void dealloc() {
		for(Model model: cache.values()) {
			model.dealloc();
		}
		
		cache.clear();
	}
}
